#region (c) 2014 THN Solutions LLC. - All rights reserved
/*
THN DATA ABSTRACTION LAYER version 1

Copyright (c) 2014, THN Solutions LLC. ( www.ThnSolutions.com )
Author: Nguyễn, M. Hải                 ( www.Minh-Hai.com     )
All rights reserved.

This library is dual-licensed.
  + For commercial software, please obtain a commercial license from THN Solutions LLC.

  + For free software, this library is licensed under GPL version 3. A summary of GPLv3 is
    listed below. You should also find a copy of GPLv3 as file License_GPLv3.txt included
	with the source-code files.

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License version 3 as published by
    the Free Software Foundation  of the License.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/
#endregion

#region Using directives
using System;
using System.Collections;
using Thn.Application;
using Thn.Data.Exceptions;
using Thn.Data.Settings;
#endregion

namespace Thn.Data.Mapping
{
    /// <summary>
    /// Abstract base class for data mappers.
    /// <para>This class provides mapping caching</para>
    /// </summary>
    public abstract class AbstractDataMapper: IDataMapper
    {
        #region Cache
        /// <summary>
        /// Caches entity mappings.
        /// <para>Key  : Object type</para>
        /// <para>Value: Object map</para>
        /// </summary>
        protected Hashtable mEntityCache = new Hashtable();

        /// <summary>
        /// Caches attribute mappings by entity
        /// <para>Object Type -> Object attribute name -> Attribute mapping</para>
        /// <para>Key  : Object type</para>
        /// <para>Value: Another hashtable whiches cache the attribute by name</para>
        /// </summary>
        protected Hashtable mEntityObjectAttributeCache = new Hashtable();

        /// <summary>
        /// Caches identifier attribute mappings by entity
        /// <para>Key  : object type</para>
        /// <para>Value: AttributeMap[] identifier</para>
        /// </summary>
        protected Hashtable mEntityIdentifierCache = new Hashtable();

        #region Add Attribute Map
        /// <summary>
        /// Add an attribute mapping to cache
        /// </summary>
        /// <param name="objectType"></param>
        /// <param name="attribute"></param>
        protected void AddAttributeMap(Type objectType, AttributeMap attribute)
        {
            Hashtable cache = null;
            
            if (!mEntityObjectAttributeCache.Contains(objectType))
            {
                //cache for entity not exists, create one
                cache = new Hashtable();
                mEntityObjectAttributeCache.Add(objectType, cache);
            }
            else
            {
                //cache exists
                cache = mEntityObjectAttributeCache[objectType] as Hashtable;

                //if attribute map exists, drop it
                if (cache.Contains(attribute.ObjectAttribute)) cache.Remove(attribute.ObjectAttribute);

                //add mapping to cache
                cache.Add(attribute.ObjectAttribute, attribute);
            }
        }
        #endregion

        #region Get Attribute Map
        /// <summary>
        /// Get the attribute map based on the object's attribute name
        /// </summary>
        /// <param name="objectType">type of object</param>
        /// <param name="name">object's attribute name</param>
        protected AttributeMap GetObjectAttributeMap(Type objectType, string name)
        {
            AttributeMap result = null;
            if (mEntityObjectAttributeCache.Contains(objectType))
            {
                Hashtable cache = mEntityObjectAttributeCache[objectType] as Hashtable;
                if (cache.Contains(name)) result = cache[name] as AttributeMap;
            }

            return result;
        }
        #endregion
        #endregion        

        #region Create Attribute Map Instance
        /// <summary>
        /// Construct a new instance of AttributeMap
        /// </summary>
        protected abstract AttributeMap CreateAttributeMapInstance();
        #endregion

        #region Get Entity
        /// <summary>
        /// Gets mapping for an object type
        /// </summary>
        protected abstract ObjectMap OnGetEntity(Type objectEntity);
        
        /// <summary>
        /// Gets the mapping for an oject type
        /// </summary>
        public ObjectMap GetEntity(Type objectEntity)
        {
            ObjectMap result = null;
            if (mEntityCache.Contains(objectEntity))
            {
                //already in cache, get it
                result = mEntityCache[objectEntity] as ObjectMap;
            }
            else
            {
                //not in cache yet, ask my descendant to do it
                result = OnGetEntity(objectEntity);

                //cache for later
                mEntityCache.Add(objectEntity, result);
            }

            return result;
        }
        #endregion

        #region Get Attribute
        /// <summary>
        /// Gets an attribute map by name
        /// </summary>
        /// <param name="objectEntity">type of object</param>
        /// <param name="name">name of object attribute</param>
        public AttributeMap GetAttribute(Type objectEntity, string name)
        {
            AttributeMap result = null;

            result = GetObjectAttributeMap(objectEntity, name);

            if (result == null)
            {
                //did not find in cache, perhaps I haven't retrieve attributes of this type
                GetAttributes(objectEntity);

                //let's try again
                result = GetObjectAttributeMap(objectEntity, name);

                if (result == null)
                {
                    //still not found, let's throw exception
                    AttributeNotFoundException error = new AttributeNotFoundException(objectEntity, name);
                    if (DataTierSettings.AutoPublishError) Log.Error(error);
                    if (!DataTierSettings.SilentError) throw error;
                }
            }

            return result;
        }
        #endregion

        #region Get Attributes
        /// <summary>
        /// Gets savable attribute(fields, properties) for a given object type
        /// </summary>
        protected abstract AttributeMap[] OnGetAttributes(Type objectEntity);
        
        /// <summary>
        /// Gets savable attribute(fields, properties) for a given object type
        /// </summary>
        public AttributeMap[] GetAttributes(Type objectEntity)
        {
            AttributeMap[] result = null;

            if (mEntityObjectAttributeCache.Contains(objectEntity))
            {
                //already exists, get it
                Hashtable hash = mEntityObjectAttributeCache[objectEntity] as Hashtable;

                result = new AttributeMap[hash.Count];
                hash.Values.CopyTo(result, 0);
            }
            else
            {
                //not exists, ask my descendant to do it
                result = OnGetAttributes(objectEntity);
                ArrayList identifiers = new ArrayList();

                //add to cache
                Hashtable hash = new Hashtable();
                for (int i = 0; i < result.Length; i++)
                {
                    hash.Add(result[i].ObjectAttribute, result[i]);
                    if (result[i].IsIdentifier) identifiers.Add(result[i]);
                }
                mEntityObjectAttributeCache.Add(objectEntity, hash);

                //cache identifier map
                AttributeMap[] identifierMaps = new AttributeMap[identifiers.Count];
                identifiers.CopyTo(identifierMaps);
                if (mEntityIdentifierCache.Contains(objectEntity)) mEntityIdentifierCache.Remove(objectEntity);
                mEntityIdentifierCache.Add(objectEntity, identifierMaps);

            }

            return result;
        }
        #endregion

        #region Get Identifiers
        /// <summary>
        /// Gets the identifier attribute(s) for a given object type
        /// </summary>
        protected virtual AttributeMap[] OnGetIdentifiers(Type objectEntity)
        {
            AttributeMap[] result = null;

            if (mEntityIdentifierCache.Contains(objectEntity))
            {
                result = (AttributeMap[])mEntityIdentifierCache[objectEntity];
            }
            else
            {
                //not in cache yet, perhaps I haven't retrieve attributes of this type
                GetAttributes(objectEntity);

                //let's try again
                if (mEntityIdentifierCache.Contains(objectEntity))
                {
                    result = (AttributeMap[])mEntityIdentifierCache[objectEntity];
                }
                else result = new AttributeMap[0];
            }

            return result;
        }

        /// <summary>
        /// Gets the identifier attribute(s) for a given object type
        /// </summary>
        public AttributeMap[] GetIdentifiers(Type objectEntity)
        {
            return OnGetIdentifiers(objectEntity);
        }
        #endregion

        #region Set Object Attribute value
        /// <summary>
        /// Assign value to object's attribute
        /// </summary>
        /// <param name="target">object to set</param>
        /// <param name="attribute">name of object's attribute</param>
        /// <param name="value">value to set</param>
        public abstract void SetObjectAttributeValue(object target, string attribute, object value);

        /// <summary>
        /// Assign value to object's attribute
        /// </summary>
        /// <param name="target">object to set</param>
        /// <param name="attribute">attribute map to assign value</param>
        /// <param name="value">value to set</param>
        public abstract void SetObjectAttributeValue(object target, AttributeMap attribute, object value);
        #endregion

        #region Get Object Attribute Value
        /// <summary>
        /// Get value from object's attribute and return (in data attribute type) value
        /// </summary>
        /// <param name="target">object to obtain value from</param>
        /// <param name="attribute">name of object's attribute</param>
        /// <returns>value in data attribute's type</returns>
        public abstract object GetObjectAttributeValue(object target, string attribute);

        /// <summary>
        /// Get value from object's attribute
        /// </summary>
        /// <param name="target">object to obtain value from</param>
        /// <param name="attribute">attribute map to obtain value</param>
        public abstract object GetObjectAttributeValue(object target, AttributeMap attribute);
        #endregion
    }
}

